const isTextNode = (node) => node.nodeType === Node.TEXT_NODE;
const isElementNode = (node) => node.nodeType === Node.ELEMENT_NODE;
const isDirective = (attrName) => attrName.startsWith('v-');

const deconstructDirective = (attrName) => {
  const regexp = /^v-(\w+)(:(\w+))?/;
  const [_, name, __, arg] = regexp.exec(attrName);
  return { name, arg };
}


class Directives {
  constructor(vm) {
    this.vm = vm;
  }

  text(node, binding) {
    node.textContent = this.vm[binding.value];

    new Watcher(this.vm, binding.value, newValue => {
      node.textContent = newValue;
    })

    node.removeAttribute(`v-text`);
  }

  /**
   * @param {Element} node 
   */
  model(node, binding) {
    node.value = this.vm[binding.value];
    const self = this;
    node.addEventListener('input', function (e) {
      self.vm[binding.value] = e.target.value;
    });

    new Watcher(this.vm, binding.value, (newValue) => {
      node.value = newValue
    });

    node.removeAttribute(`v-model`);
  }

  /**
   * @param {Element} node 
   */
  bind(node, binding) {
    node.setAttribute(binding.arg, this.vm[binding.value]);

    new Watcher(this.vm, binding.value, (newValue) => {
      node.setAttribute(binding.arg, newValue);
    })

    node.removeAttribute(`v-bind:${binding.arg}`);
  }

  /**
   * @param {Element} node
   */
  on(node, binding) {
    node.addEventListener(binding.arg, this.vm[binding.value]);

    new Watcher(this.vm, binding.value, (newValue, oldValue) => {
      node.removeEventListener(binding.arg, oldValue);
      node.addEventListener(binding.arg, newValue);
    })

    node.removeAttribute(`v-on:${binding.arg}`);
  }

  show(node, binding) {
    const rawDisplay = node.style.display;
    node.style.display = Boolean(this.vm[binding.value]) ? rawDisplay : 'none';

    new Watcher(this.vm, binding.value, (newValue) => {
      node.style.display = Boolean(newValue) ? rawDisplay : 'none';
    })

    node.removeAttribute(`v-show`);
  }
};

class Compiler {
  constructor(vm) {
    this.vm = vm;
    this.el = vm.$el;
    this.directives = new Directives(vm);

    this.compile(this.el);
  }

  compile(el) {
    const childNodes = [...el.childNodes];
    childNodes.forEach(node => {
      if (isTextNode(node)) { this.compileText(node) }
      else if (isElementNode(node)) { this.compileElement(node) }

      if (node.childNodes && node.childNodes.length) {
        this.compile(node);
      }
    })
  }

  /**
   * 处理文本节点，mustache表达式
   * @param {Node} node 
   */
  compileText(node) {
    const self = this;
    const regexp = /\{\{(.+)\}\}/g;
    const rawContent = node.textContent;

    let res = null;
    const matches = [];
    while (res = regexp.exec(rawContent)) {
      matches.push(res);
    }
    node.textContent = matches.reduce((acc, [mark, value]) => {
      const key = value.trim();
      acc = acc.replace(mark, self.vm[key]);

      new Watcher(self.vm, key, newValue => { // 只要有一个变了，全部遍历更新
        node.textContent = matches.reduce((acc, [mark, value]) => {
          const key = value.trim();
          acc = acc.replace(mark, self.vm[key]);
          return acc;
        }, rawContent);
      });

      return acc;
    }, rawContent);

  }

  /**
   * @param {Element} node 
   */
  compileElement(node) {
    [...node.attributes].forEach(attr => {
      if (isDirective(attr.name)) {
        this.doDirective(node, attr.name, attr.value)
      }
    })
  }

  doDirective(node, attrName, attrValue) {
    const { name, arg } = deconstructDirective(attrName);
    this.directives[name]?.(node, { arg, value: attrValue });
  }
}